/*
 * $Id$
 * $HeadURL$
 *
 * This file is a part of ultralog project (http://code.google.com/p/ultralog/).
 * Released under New BSD License.
 */
package com.google.code.ultralog.formatter.gfree;

import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;

import com.google.code.ultralog.formatter.LongFormatter;
import com.google.code.ultralog.utils.CharacterOutputStream;

/**
 * Garbage free implementation of {@link LongFormatter} interface that treats
 * long values as timestamps and formats them according to specified pattern.
 * Pattern is a string that may contain the following placeholders that will be
 * replaced with value derived from timestamp:
 *
 * <table border="1">
 *   <tr>
 *     <th>Placeholder</th>
 *     <th>Value</th>
 *   </tr>
 *   <tr>
 *     <td>%a</td>
 *     <td>Abbreviated weekday name ("Sun", "Mon", "Tue", ...)</td>
 *   </tr>
 *   <tr>
 *     <td>%b</td>
 *     <td>Abbreviated month name ("Jan", "Feb", "Mar", ...)</td>
 *   </tr>
 *   <tr>
 *     <td>%c</td>
 *     <td>Month numeric (1 .. 12)</td>
 *   </tr>
 *   <tr>
 *     <td>%d</td>
 *     <td>Day of month numeric (01 .. 31)</td>
 *   </tr>
 *   <tr>
 *     <td>%e</td>
 *     <td>Day of month numeric (1 .. 31)</td>
 *   </tr>
 *   <tr>
 *     <td>%f</td>
 *     <td>Milliseconds (0 .. 999)</td>
 *   </tr>
 *   <tr>
 *     <td>%F</td>
 *     <td>Milliseconds (000 .. 999)</td>
 *   </tr>
 *   <tr>
 *     <td>%H</td>
 *     <td>Hour (00 .. 23)</td>
 *   </tr>
 *   <tr>
 *     <td>%h</td>
 *     <td>Hout (01 .. 12)</td>
 *   </tr>
 *   <tr>
 *     <td>%i</td>
 *     <td>Minute (00 .. 59)</td>
 *   </tr>
 *   <tr>
 *     <td>%j</td>
 *     <td>Day of year (001 .. 366)</td>
 *   </tr>
 *   <tr>
 *     <td>%k</td>
 *     <td>Hour (0 .. 23)</td>
 *   </tr>
 *   <tr>
 *     <td>%l</td>
 *     <td>Hour (1 .. 12)</td>
 *   </tr>
 *   <tr>
 *     <td>%M</td>
 *     <td>Month name ("January", "February", "March")</td>
 *   </tr>
 *   <tr>
 *     <td>%m</td>
 *     <td>Month numeric (01 .. 12)</td>
 *   </tr>
 *   <tr>
 *     <td>%p</td>
 *     <td>AM or PM</td>
 *   </tr>
 *   <tr>
 *     <td>%S</td>
 *     <td>Second (0 .. 59)</td>
 *   </tr>
 *   <tr>
 *     <td>%s</td>
 *     <td>Second (00 .. 59)</td>
 *   </tr>
 *   <tr>
 *     <td>%W</td>
 *     <td>Weekday name ("Sunday", "Monday", "Tuesday")</td>
 *   </tr>
 *   <tr>
 *     <td>%w</td>
 *     <td>Day of week (1 - Sunday .. 7 - Saturday)</td>
 *   </tr>
 *   <tr>
 *     <td>%Y</td>
 *     <td>Year (4 digits)</td>
 *   </tr>
 *   <tr>
 *     <td>%y</td>
 *     <td>Year (2 digits)</td>
 *   </tr>
 * </table>
 *
 * <p>Here is an example of pattern: "%Y-%m-%d %H:%i:%s.%F".
 *
 * This implementation does not support negative timestamps.
 *
 * @author Mikhail Vladimirov
 */
public class GarbageFreeTimestampFormatter implements LongFormatter
{
    private final static ThreadLocal <ExtractedYearMonth> EXTRACTED_YEAR_MONTH =
        new ThreadLocal <ExtractedYearMonth> ()
        {
            @Override
            protected ExtractedYearMonth initialValue()
            {
                return new ExtractedYearMonth ();
            };
        };

    private final static long SECOND = 1000L;
    private final static long MINUTE = SECOND * 60L;
    private final static long HOUR = MINUTE * 60L;
    private final static long DAY = HOUR * 24L;

    private final static long [] MONTH = new long [] {
        DAY * 31L,
        DAY * 28L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L
    };

    private final static long [] LEAP_MONTH = new long [] {
        DAY * 31L,
        DAY * 29L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L,
        DAY * 30L,
        DAY * 31L
    };

    private final static long YEAR = DAY * 365L;
    private final static long LEAP_YEAR = DAY * 366L;
    private final static long YEAR_4 = YEAR * 3L + LEAP_YEAR;
    private final static long LEAP_YEAR_4 = YEAR * 4L;
    private final static long YEAR_28 = YEAR_4 * 7L;
    private final static long YEAR_100 = YEAR_4 * 24L + LEAP_YEAR_4;
    private final static long LEAP_YEAR_100 = YEAR_4 * 25L;
    private final static long LEAP_YEAR_200 = LEAP_YEAR_100 + YEAR_100;
    private final static long LEAP_YEAR_300 = LEAP_YEAR_100 + YEAR_100 * 2L;
    private final static long YEAR_400 = YEAR_100 * 3L + LEAP_YEAR_100;

    private final LongFormatter [] chunks;
    private TimeZone tz;

    /**
     * Create new formatter that formats timestamps according to given pattern
     * and time zone.
     *
     * @param pattern pattern to use for formatting timestamps
     * @param tz time zone to use or <code>null</code> to use UTC time zone.
     */
    public GarbageFreeTimestampFormatter (String pattern, TimeZone tz)
    {
        if (pattern == null)
            throw new IllegalArgumentException ("Pattern is null");

        this.tz = tz;

        List <LongFormatter> chunks = new ArrayList <LongFormatter> ();

        int length = pattern.length ();
        int state = 0;
        StringBuilder literal = new StringBuilder ();
        for (int i = 0; i < length; i++)
        {
            char ch = pattern.charAt (i);

            switch (state)
            {
            case 0: // Initial state
                switch (ch)
                {
                case '%':
                    state = 1;
                    break;
                default:
                    literal.append (ch);
                }
                break;
            case 1: // After procent sign
                switch (ch)
                {
                case 'a':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (AbbreviatedWeekdayName.INSTANCE);
                    break;
                case 'b':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (AbbreviatedMonthName.INSTANCE);
                    break;
                case 'c':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (MonthNumeric.INSTANCE);
                    break;
                case 'd':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (DayOfMonthNumeric2.INSTANCE);
                    break;
                case 'e':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (DayOfMonthNumeric.INSTANCE);
                    break;
                case 'f':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Milliseconds.INSTANCE);
                    break;
                case 'F':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Milliseconds3.INSTANCE);
                    break;
                case 'H':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Hour24_2.INSTANCE);
                    break;
                case 'h':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Hour12_2.INSTANCE);
                    break;
                case 'i':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Minutes2.INSTANCE);
                    break;
                case 'j':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (DayOfYear3.INSTANCE);
                    break;
                case 'k':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Hour24.INSTANCE);
                    break;
                case 'l':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Hour12.INSTANCE);
                    break;
                case 'M':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (MonthName.INSTANCE);
                    break;
                case 'm':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (MonthNumeric2.INSTANCE);
                    break;
                case 'p':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (AMPM.INSTANCE);
                    break;
                case 'S':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Seconds.INSTANCE);
                    break;
                case 's':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Seconds2.INSTANCE);
                    break;
                case 'W':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (WeekdayName.INSTANCE);
                    break;
                case 'w':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (DayOfWeek.INSTANCE);
                    break;
                case 'Y':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Year4.INSTANCE);
                    break;
                case 'y':
                    if (literal.length () > 0)
                    {
                        chunks.add (new Literal (literal.toString ()));
                        literal.setLength (0);
                    }
                    chunks.add (Year2.INSTANCE);
                    break;
                default:
                    literal.append (ch);
                }
                state = 0;
                break;
            default:
                throw new Error ("Unknown state: " + state);
            }
        }

        if (state != 0)
            throw new IllegalArgumentException (
                "Unexpected end of pattern: " + pattern);

        if (literal.length () > 0)
            chunks.add (new Literal (literal.toString ()));

        this.chunks = chunks.toArray (new LongFormatter [chunks.size ()]);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void format (long value, CharacterOutputStream output)
    {
        if (output == null)
            throw new IllegalArgumentException ("Output is null");

        if (tz != null)
            value += tz.getOffset (value);

        if (value < 0)
            GarbageFreeFormatterUtils.writeLong (value, output);
        else
        {
            for (int count = chunks.length, i = 0; i < count; i++)
                chunks [i].format (value, output);
        }
    }

    private static int getMillisecond (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        return (int)(timestamp % SECOND);
    }

    private static int getSecond (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        return (int)(timestamp % MINUTE / SECOND);
    }

    private static int getMinute (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        return (int)(timestamp % HOUR / MINUTE);
    }

    private static int getHour (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        return (int)(timestamp % DAY / HOUR);
    }

    private static int getDayOfWeek (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        return (int)((timestamp / DAY + 4L) % 7L);
    }

    private static int getDayOfMonth (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        long m = extractMonth (timestamp);

        return (int)m;
    }

    private static int getDayOfYear (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        long e = extractYear (timestamp);

        return (int)e;
    }

    private static int getMonth (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        long m = extractMonth (timestamp);

        return (int)(m >>> 32);
    }

    private static int getYear (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        return (int)(extractYear (timestamp) >>> 32);
    }

    private static long extractMonth (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        ExtractedYearMonth eym = EXTRACTED_YEAR_MONTH.get ();

        if (eym.extractedMonthTimestamp == timestamp)
            return eym.extractedMonth;

        long e = extractYear (timestamp);

        int year = (int)(e >>> 32);
        long offset = ((int)e) * DAY;

        boolean leap = isLeapYear (year);

        long [] month = leap ? LEAP_MONTH : MONTH;

        for (int i = 0; i < 12; i++)
        {
            long m = month [i];

            if (offset >= m)
                offset -= m;
            else
            {
                long result = (((long)i) << 32) + (offset / DAY);

                eym.extractedMonthTimestamp = timestamp;
                eym.extractedMonth = result;

                return result;
            }
        }

        throw new Error ("Impossible");
    }

    private static long extractYear (long timestamp)
    {
        if (timestamp < 0L)
            throw new IllegalArgumentException (
                "Timestamp (" + timestamp + ") < 0");

        ExtractedYearMonth eym = EXTRACTED_YEAR_MONTH.get ();

        if (eym.extractedYearTimestamp == timestamp)
            return eym.extractedYear;

        int year = 1970 + (int)(timestamp / YEAR_400) * 400;
        timestamp %= YEAR_400;

        if (timestamp < LEAP_YEAR_100)
        {
            year += (int)(timestamp / YEAR_4) * 4;
            timestamp %= YEAR_4;
        }
        else
        {
            if (timestamp < LEAP_YEAR_200)
            {
                timestamp -= LEAP_YEAR_100;
                year += 100;
            }
            else if (timestamp < LEAP_YEAR_300)
            {
                timestamp -= LEAP_YEAR_200;
                year += 200;
            }
            else
            {
                timestamp -= LEAP_YEAR_300;
                year += 300;
            }

            if (timestamp < YEAR_28)
            {
                year += (int)(timestamp / YEAR_4) * 4;
                timestamp %= YEAR_4;
            }
            else
            {
                year += 28;
                timestamp -= YEAR_28;
                if (timestamp < LEAP_YEAR_4)
                {
                    // Do nothing
                }
                else
                {
                    year += 4;
                    timestamp -= LEAP_YEAR_4;

                    year += (int)(timestamp / YEAR_4) * 4;
                    timestamp %= YEAR_4;
                }
            }
        }

        while (true)
        {
            long yearLength = isLeapYear (year) ? LEAP_YEAR : YEAR;

            if (timestamp >= yearLength)
            {
                timestamp -= yearLength;
                year += 1;
            }
            else break;
        }

        long result = (((long)year) << 32) + (timestamp / DAY);

        eym.extractedYearTimestamp = timestamp;
        eym.extractedYear = result;

        return result;
    }

    private static boolean isLeapYear (int year)
    {
        return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    }

    private static void writeIntegerWidth (
        int value, int width, CharacterOutputStream output)
    {
        if (value < 0)
            throw new IllegalArgumentException ("Value (" + value + ") < 0");

        if (width < 0)
            throw new IllegalArgumentException ("Width (" + width + ") < 0");

        if (output == null)
            throw new IllegalArgumentException ("Output is null");

        int v = value;
        int n = 0;

        while (v != 0)
        {
            v /= 10;
            n += 1;
        }

        if (n == 0) n = 1;

        if (n < width)
        {
            int count = width - n;

            while (count >= 4)
            {
                output.write ('0', '0', '0', '0');
                count -= 4;
            }

            switch (count)
            {
            case 3:
                output.write ('0', '0', '0');
                break;
            case 2:
                output.write ('0', '0');
                break;
            case 1:
                output.write ('0');
                break;
            case 0:
                // Do nothing
                break;
            default:
                throw new Error ("Impossible");
            }
        }

        GarbageFreeFormatterUtils.writeInteger (value, output);
    }

    private class Literal implements LongFormatter
    {
        private final String literal;

        public Literal (String literal)
        {
            if (literal == null)
                throw new IllegalArgumentException ("Literal is null");

            this.literal = literal;
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            output.write (literal, 0, literal.length ());
        }
    }

    private static class AbbreviatedMonthName implements LongFormatter
    {
        private final static AbbreviatedMonthName INSTANCE =
            new AbbreviatedMonthName ();

        private final static String [] MONTH_NAMES = new String [] {
            "Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

        private AbbreviatedMonthName ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            String monthName = MONTH_NAMES [getMonth (value)];

            output.write (monthName, 0, monthName.length ());
        }
    }

    private static class AbbreviatedWeekdayName implements LongFormatter
    {
        private final static AbbreviatedWeekdayName INSTANCE =
            new AbbreviatedWeekdayName ();

        private final static String [] WEEKDAY_NAMES = new String [] {
            "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};

        private AbbreviatedWeekdayName ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            String weekdayName = WEEKDAY_NAMES [getDayOfWeek (value)];

            output.write (weekdayName, 0, weekdayName.length ());
        }
    }

    private static class AMPM implements LongFormatter
    {
        private final static AMPM INSTANCE =
            new AMPM ();

        private AMPM ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int hour = getHour (value);

            String ampm = hour < 12 ? "AM" : "PM";

            output.write (ampm, 0, ampm.length ());
        }
    }

    private static class DayOfMonthNumeric implements LongFormatter
    {
        private final static DayOfMonthNumeric INSTANCE =
            new DayOfMonthNumeric ();

        private DayOfMonthNumeric ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int dayOfMonth = getDayOfMonth (value);

            GarbageFreeFormatterUtils.writeInteger (dayOfMonth + 1, output);
        }
    }

    private static class DayOfMonthNumeric2 implements LongFormatter
    {
        private final static DayOfMonthNumeric2 INSTANCE =
            new DayOfMonthNumeric2 ();

        private DayOfMonthNumeric2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int dayOfMonth = getDayOfMonth (value);

            writeIntegerWidth (dayOfMonth + 1, 2, output);
        }
    }

    private static class DayOfWeek implements LongFormatter
    {
        private final static DayOfWeek INSTANCE =
            new DayOfWeek ();

        private DayOfWeek ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int dayOfWeek = getDayOfWeek (value);

            GarbageFreeFormatterUtils.writeInteger (dayOfWeek + 1, output);
        }
    }

    private static class DayOfYear3 implements LongFormatter
    {
        private final static DayOfYear3 INSTANCE =
            new DayOfYear3 ();

        private DayOfYear3 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int dayOfYear = getDayOfYear (value);

            writeIntegerWidth (dayOfYear + 1, 3, output);
        }
    }

    private static class Hour12 implements LongFormatter
    {
        private final static Hour12 INSTANCE =
            new Hour12 ();

        private Hour12 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int hour = getHour (value) % 12;

            if (hour == 0) hour = 12;

            GarbageFreeFormatterUtils.writeInteger (hour, output);
        }
    }

    private static class Hour12_2 implements LongFormatter
    {
        private final static Hour12_2 INSTANCE =
            new Hour12_2 ();

        private Hour12_2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int hour = getHour (value) % 12;

            if (hour == 0) hour = 12;

            writeIntegerWidth (hour, 2, output);
        }
    }

    private static class Hour24 implements LongFormatter
    {
        private final static Hour24 INSTANCE =
            new Hour24 ();

        private Hour24 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int hour = getHour (value);

            GarbageFreeFormatterUtils.writeInteger (hour, output);
        }
    }

    private static class Hour24_2 implements LongFormatter
    {
        private final static Hour24_2 INSTANCE =
            new Hour24_2 ();

        private Hour24_2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int hour = getHour (value);

            writeIntegerWidth (hour, 2, output);
        }
    }

    private static class Milliseconds implements LongFormatter
    {
        private final static Milliseconds INSTANCE =
            new Milliseconds ();

        private Milliseconds ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int milliseconds = getMillisecond (value);

            GarbageFreeFormatterUtils.writeInteger (milliseconds, output);
        }
    }

    private static class Milliseconds3 implements LongFormatter
    {
        private final static Milliseconds3 INSTANCE =
            new Milliseconds3 ();

        private Milliseconds3 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int milliseconds = getMillisecond (value);

            writeIntegerWidth (milliseconds, 3, output);
        }
    }

    private static class Minutes2 implements LongFormatter
    {
        private final static Minutes2 INSTANCE =
            new Minutes2 ();

        private Minutes2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            int minutes = getMinute (value);

            writeIntegerWidth (minutes, 2, output);
        }
    }

    private static class MonthName implements LongFormatter
    {
        private final static MonthName INSTANCE =
            new MonthName ();

        private final static String [] MONTH_NAMES = new String [] {
            "January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December"
        };

        private MonthName ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            String monthName = MONTH_NAMES [getMonth (value)];

            output.write (monthName, 0, monthName.length ());
        }
    }

    private static class MonthNumeric implements LongFormatter
    {
        private final static MonthNumeric INSTANCE =
            new MonthNumeric ();

        private MonthNumeric ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            GarbageFreeFormatterUtils.writeInteger (
                getMonth (value) + 1, output);
        }
    }

    private static class MonthNumeric2 implements LongFormatter
    {
        private final static MonthNumeric2 INSTANCE =
            new MonthNumeric2 ();

        private MonthNumeric2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            writeIntegerWidth (getMonth (value) + 1, 2, output);
        }
    }

    private static class Seconds implements LongFormatter
    {
        private final static Seconds INSTANCE =
            new Seconds ();

        private Seconds ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            GarbageFreeFormatterUtils.writeInteger (
                getSecond (value), output);
        }
    }

    private static class Seconds2 implements LongFormatter
    {
        private final static Seconds2 INSTANCE =
            new Seconds2 ();

        private Seconds2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            writeIntegerWidth (getSecond (value), 2, output);
        }
    }

    private static class WeekdayName implements LongFormatter
    {
        private final static WeekdayName INSTANCE =
            new WeekdayName ();

        private final static String [] WEEKDAY_NAMES = new String [] {
            "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
            "Saturday"};

        private WeekdayName ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            String weekdayName = WEEKDAY_NAMES [getDayOfWeek (value)];

            output.write (weekdayName, 0, weekdayName.length ());
        }
    }

    private static class Year2 implements LongFormatter
    {
        private final static Year2 INSTANCE =
            new Year2 ();

        private Year2 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            writeIntegerWidth (getYear (value) % 100, 2, output);
        }
    }

    private static class Year4 implements LongFormatter
    {
        private final static Year4 INSTANCE =
            new Year4 ();

        private Year4 ()
        {
            // Do nothing
        }

        @Override
        public void format (long value, CharacterOutputStream output)
        {
            if (output == null)
                throw new IllegalArgumentException ("Output is null");

            writeIntegerWidth (getYear (value), 4, output);
        }
    }

    private static class ExtractedYearMonth
    {
        public long extractedYearTimestamp = Long.MIN_VALUE;
        public long extractedYear = Long.MIN_VALUE;
        public long extractedMonthTimestamp = Long.MIN_VALUE;
        public long extractedMonth = Long.MIN_VALUE;
    }
}
